//License: Artistic License v2.0, http://opensource.org/licenses/Artistic-2.0

/*
	Package misc contains convenience wrappers on:
	
		- geolocation identification based on IP address
		- mobile browser detection based on HTTP request
		- RPC server and caller
		- net channel listener that writes incoming string slices to file
		- JSON file handling
		- string (password) encoding & decoding
		- slice conversion to string
*/
package misc

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/json"
	"encoding/csv"
	"fmt"
	"io/ioutil"
	"log"
	"math/rand"
	"net"
	"net/http"
	"net/rpc"
	"old/netchan"
	"os"
	"regexp"
	"sort"
	"strconv"
	"strings"
	"time"
)

var netCh = make(chan []string) //netchan for http logging
var ipLocationTable [][]string  //table of (numerical) ip addr and location
var ipSearchRange []float64     //numerical ip addr range used for searching
var mobileRegexp regexp.Regexp  //regexp for mobile browser identification

//GetLocation searches the ip addr string of format w.x.y.z in the ip location table
//that is constructed in SetIPLocationTable beforehand.
//and returns city, area/state, ISO code as string according to the raw data CSV file.
//User is responsible to pick whichever column / field contains the relevant location
//info based on the data set from the data provider. Not all data file is the same.
//If raw data file does not have the specific data point you want (e.g. city, area),
//then please give it a -1 as in the example below.
//
//For example:
//If using raw data file DB4 from ip2location.com, then you can have country, area/state,
//and city.  The data looks like this:
//"201672192","201672223","US","UNITED STATES","TEXAS","DALLAS","".
//Calling GetIPLocation("w.x.y.z", 5, 4, 2) will return "DALLAS, TEXAS, US".
//If using raw data from software77.net, the data file only has country.
//The data looks like this:
//"49807360","50331647","","1293667200","SE","SWE","Sweden".
//Calling GetIPLocation("w.x.y.z", 4, -1, -1) will return "SE" or
//GetIPLocation("w.x.y.z", 6, -1, -1) will return "Sweden".
func GetIPLocation(str string, city, area, country int) string {
	var ip float64
	if len(ipSearchRange) == 0 || len(ipLocationTable) == 0 {
		return ""
	}
	s := strings.Split(str, ":") //ip may include port # that we need to drop
	t := strings.Split(s[0], ".")
	i, err := strconv.ParseFloat(t[3], 64)
	if err != nil {
		return ""
	}
	ip = i
	i, err = strconv.ParseFloat(t[2], 64)
	if err != nil {
		return ""
	}
	ip = ip + (i * 256)
	i, err = strconv.ParseFloat(t[1], 64)
	if err != nil {
		return ""
	}
	ip = ip + (i * 256 * 256)
	i, err = strconv.ParseFloat(t[0], 64)
	if err != nil {
		return ""
	}
	ip = ip + (i * 256 * 256 * 256)
	idx := sort.SearchFloat64s(ipSearchRange, ip)
	var locationStr string
	columnCount := len(ipLocationTable[0])
	if city >= 0 && city < columnCount {
		locationStr = ipLocationTable[idx][city]
	}
	if area >= 0 && area < columnCount {
		locationStr = locationStr + ", " + ipLocationTable[idx][area]
	}
	if country >= 0 && country < columnCount {
		locationStr = locationStr + ", " + ipLocationTable[idx][country]
	}
	return locationStr
}

//SetLocationTable loads the IP location raw data from CSV file into memory
//for the consumption of GetLocation.  It reads CSV file with ALL of the header removed.
//The raw data can come from http://Software77.net, http://ip2location.com, et cetera.
//Different data provider and different data set will contain different things.
//Some contain only country location.  Others contain other details, e.g city, state.
//(Please note data provider's license term on the data they provide.)
//
//The raw data will contain "IP From" and "IP To" columns. ipTo is the column number
//of the "IP To" column (zero-based index).  Usually it is the second column, i.e.
//ipTo == 1, but it is user-defined to accommodate weird raw data file.
//Warning: Depending on the raw data set, the CSV file may be large and, accordingly, its
//memory consumption.  You can reduce the size by removing unnecessary columns from the
//CSV file, but do not forget to set your calling parameters in GetIPLocation accordingly.
func SetIPLocationTable (fname string, ipTo int) (err error) {
	if ipTo < 0 {
		return nil //TODO: register a new err
	}
	f, err := os.OpenFile(fname, os.O_RDONLY, 0600)
	if err != nil {
		return err
	}
	defer f.Close()
	csvData := csv.NewReader(f)
	ipLocationTable, err = csvData.ReadAll()
	if err != nil {
		return err
	}
	ipSearchRange = make ([]float64, 0) //must be reset to enable hot swap / live-update
	for i := 0; i < len(ipLocationTable); i++ {
		ip, err := strconv.ParseFloat(ipLocationTable[i][ipTo], 64)
		if err != nil {
			return err
		}
		ipSearchRange = append(ipSearchRange, ip)
	}
	return nil
}

//IsMobile checks an input string against the mobile regexp
//constructed in SetMobileRegexp beforehand.  The input string is typically the
//user browser's signature / agent.  If regexp matches, then IsMobile returns true.
//For example: IsMobile(http.Request.UserAgent())
func IsMobile(userAgent string) bool {
	return mobileRegexp.MatchString(strings.ToLower(userAgent))
}

//SetMobileRegexp loads Go regular expression from a text file (fname) into memory
//for the consumption of IsMobile. You typically call it during app start up,
//not during runtime, since it reads from disk.
//For example: The text file may contain "android.+mobile|ip(hone|od)".
func SetMobileRegexp(fname string) (err error) {
	byteArray, err := ioutil.ReadFile(fname)
	if err != nil {
		return (err)
	}
	re, err := regexp.Compile(string(byteArray))
	if err != nil {
		return (err)
	}
	mobileRegexp = *re
	return nil
}

//CallRPC calls the func rpcName at connType and connAddr.
//For example: CallRPC("tcp", "w.x.y.z:1234", "Admin.Do", "backup") where
//"Admin" is the data type containing the func to be called remotely and "Do"
//is the name of the func / procedure and "backup" is the parameter to be passed.
func CallRPC(connType, connAddr, rpcName string, rpcArgs interface{}) error {
	client, err := rpc.DialHTTP(connType, connAddr)
	if err != nil {
		return err
	}
	call := new(rpc.Call)
	call.Args = rpcArgs
	return client.Call(rpcName, call.Args, nil)
}

//ListenAndServeRPC executes RPC via receiver at connType and connAddr.
//For example: ListenAndServeRPC(new(Admin), "tcp", "w.x.y.z:1234") where
//"Admin" is the data type containing the func to be called remotely.
//The RPC standard lib forces RPC calls to be done through exported data type.
func ListenAndServeRPC(receiver interface{}, connType, connAddr string) {
	rpc.Register(receiver)
	rpc.HandleHTTP()
	listener, err := net.Listen(connType, connAddr)
	if err != nil {
		log.Fatal(err)
	}
	http.Serve(listener, nil)
}

//InboundChannelToFile writes messages ([]string) sent over netchan to file(s)
//saved in folderPath.
//
//The saved file name would be the concatenation of fileName and
//Unix time and fileType. The size per file is defined by channelBufSize.
//When it reaches the planned size, the file is closed and the next file is created.
//writeImmediately determines whether to write every message that comes through
//netchan (i.e. ensures everything is written, but this is disk-bound performance)
//or to write only when it has
//received channelBufSize number of messages (i.e. if server is stopped, then
//some messages that are still in buffer are not written to disk).  You can set it
//to false and lower the buffer size to reduce the potential lost.
//
//For example: InboundChannelToFile("log","srv", []string, 10000, true).
//Pre-requisites: You must establish channel beforehand via SetInboundChannel.
//InboundChannelToFile is designed to run in a separate server from the
//sender to offload performance hit on the outbound server.
func InboundChannelToFile(
folderPath, fileName, fileType string, channelBufSize int, writeImmediately bool) {
	for {
		fname := fmt.Sprintf("%s%s%s.%s",
			folderPath, fileName, strconv.FormatInt(time.Now().Unix(), 10), fileType)
		f, err := os.OpenFile(fname, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
		if err != nil {
			log.Println(err)
		}
		if fileType == "csv" {
			csvLog := csv.NewWriter(f)
			for i := 0; i < channelBufSize; i++ {
				csvLog.Write(<-netCh)
				if writeImmediately {
					csvLog.Flush()
				}
			}
			if !writeImmediately {
				csvLog.Flush()
			}
		} //else other file types
		f.Close() //need to specifically close now, not defer, in case of high traffic
	}
}

//ToOutboundChannel sends slice of string to channel that is established in SetOutboundChannel.
func ToOutboundChannel(msg []string) {
	netCh <- msg
}

//SetOutboundChannel sets up outbound netchan at network and addr.
func SetOutboundChannel(chName, connType, connAddr string) (err error) {
	outbound := netchan.NewExporter()
	err = outbound.ListenAndServe(connType, connAddr)
	if err != nil {
		return err
	}
	return outbound.Export(chName, netCh, netchan.Send)
}

//SetInboundChannel sets up inbound netchan at connType, connAddr.  The netchan has a
//buffer of chBuf.  Inbound netchan complains if the outbound netchan is not yet up,
//so SetInboundChannel will loop every wait seconds until the outbound netchan is up.
//When netchan.Import does not establish connection, sometimes err == nil, so the
//surest way is to wait until outbound netchan is up and running before executing
//SetInboundChannel.  If outbound netchan is reset, you need to reset inbound channel too.
func SetInboundChannel(chName, connType, connAddr string, chBuf, wait int) (err error) {
	inbound := new(netchan.Importer)
	for {
		inbound, err = netchan.Import(connType, connAddr)
		if err != nil {
			log.Println(err)
			log.Printf("Retrying ... in ~ %ds\n", wait)
			time.Sleep(time.Duration(wait) * time.Second)
		} else {
			break
		}
	}
	for {
		err = inbound.Import(chName, netCh, netchan.Recv, chBuf)
		if err != nil { //catches netchan.Import or inbound.Import err
			log.Println(err)
			log.Printf("Retrying ... in ~ %ds\n", wait)
			time.Sleep(time.Duration(wait) * time.Second)
		} else {
			break
		}
	}
	return nil
}

//WriteJson is a convenience wrapper to write data structure to JSON file.
func WriteJson(obj interface{}, targetFileName string) error {
	byteArray, err := json.MarshalIndent(obj, "", "")
	if err != nil {
		return err
	}
	err = ioutil.WriteFile(targetFileName, byteArray, 0600)
	return err
}

//ReadJson is a convenience wrapper to load JSON file into data structure.
func ReadJson(sourceFileName string, obj interface{}) error {
	buf, err := ioutil.ReadFile(sourceFileName)
	if err != nil {
		return err
	}
	err = json.Unmarshal(buf, obj) //very sensitive to non well formed json
	return err
}

//EncodeString encodes a string into a random key and a hash.
//For example, this can be used to store a password.
func EncodeString(secret string) (key string, hash []byte) {
	rand.Seed(time.Now().Unix())
	key = strconv.Itoa(rand.Int())
	h := hmac.New(sha256.New, []byte(key+secret))
	hash = h.Sum(nil)
	return
}

//EncodingIsOk checks if the given string, if decoded, will match its key and hash.
//For example, this can be used to authenticate user-entered password.
func EncodingIsOk(secret, key string, hash []byte) bool {
	h := hmac.New(sha256.New, []byte(key+secret))
	s := h.Sum(nil)
	if len(s) != len(hash) {
		return false
	}
	for i := 0; i < len(s); i++ {
		if s[i] != hash[i] {
			return false
		}
	}
	return true
}

//SliceToString is a convenience wrapper to convert []string into string.
func SliceToString(slice []string) (str string) {
	for _, val := range slice {
		str = str + val
	}
	return
}

//Copyright: Slamet Hendry
//Thank you:
//Rick Morrison updated lib usage from r60 to weekly.2012-02-22
//	- hmac: NewSHA256 to New